#ifndef OPENGLPAINTINGAREA_H
#define OPENGLPAINTINGAREA_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Compatibility>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QtMath>

#define PI 3.141592653
#define CIRCLE_POINTS_NUM 20 // number of points in a quarter circle
#define CURVE_POINTS_NUM 100 // number of points in a curve

inline double C(int n, int i) {
    // n!/(i! * (n - i)!)
    i = i < (n - i) ? i : (n- i);
    double result = 1.0;
    for (int k = 1; k <= i; ++k) {
        result /= k;
        result *= n - k + 1;
    } //qDebug() << n << i << result;
    return result;
}

namespace Node {

struct ControlMode {
    GLshort type;      // 0: disable; 1: paint; 2: scale; 3: move; 4: X-scale; 5: Y-scale
    QPointF init_point;
    GLboolean pick_a_pic;
    GLboolean init_point_fix;
    GLboolean scale_a_pic;
};

struct PainterState {
    GLshort type;   // 0 : an empty node, meaningless to graph
                    // 1 : triangle
                    // 2 : rectangle
                    // 3 : line
                    // 4 : rounded rectangle
                    // 5 : circle
                    // 6 : ellipse
                    // 7 : polygon
                    // 8 : curve
    GLdouble color_R;
    GLdouble color_G;
    GLdouble color_B;
    GLshort render_type; // 0: fill; 1: line； 2： fill-with-border
};

class Pic : protected QOpenGLFunctions_3_3_Compatibility
{
public:
    Pic * next;
    Pic * prev; // doubly link list : maybe used for change-mode
    GLfloat * verts;
    GLint v_size;
    GLushort * indices;
    GLint i_size;
    GLfloat rgb[3];
    GLshort type;
    GLshort render_type;
    GLuint VAO;            // 顶点数组对象：Vertex Array Object，VAO
    GLuint  VBO;           // 顶点缓冲对象：Vertex Buffer Object，VBO
    GLuint  EBO;           // 索引缓冲对象：Element Buffer Object，EBO或Index Buffer Object，IBO
    GLboolean hasVAO;      // 判断是否需要生成VAO

    // 画曲线使用
    GLuint curVAO, curVBO, curEBO;
    GLboolean hasCurVAO;
    GLfloat * curveVertex;
    GLushort * curIndice;

    Pic() {
        next = NULL;
        prev = NULL;
        verts = NULL;
        v_size = 0;
        indices = NULL;
        i_size = 0;
        rgb[0] = 0.0f; rgb[1] = 0.0f; rgb[2] = 0.0f;
        hasVAO = false;

        curveVertex = NULL;
        curIndice = NULL;
        hasCurVAO = false;
    }
    Pic(const Pic& p) {
        next = p.next;
        prev = p.prev;
        type = p.type;
        render_type = p.render_type;
        v_size = p.v_size;
        i_size = p.i_size;
        verts = new GLfloat[v_size/sizeof(GLfloat)];
        for (unsigned int i = 0; i < v_size/sizeof(GLfloat); ++i)
            verts[i] = p.verts[i];
        indices = new GLushort[i_size/sizeof(GLushort)];
        for (unsigned int i = 0; i < i_size/sizeof(GLushort); ++i)
            indices[i] = p.indices[i];
        hasVAO = false;
        rgb[0] = p.rgb[0];
        rgb[1] = p.rgb[1];
        rgb[2] = p.rgb[2];

        curveVertex = NULL;
        curIndice = NULL;
        hasCurVAO = false;
    }

    ~Pic() {
        if (type != 0) {
            if (verts != NULL) delete verts;
            if (indices != NULL) delete indices;
            verts = NULL;
            indices = NULL;
            if (curveVertex != NULL) delete curveVertex;
            if (curIndice != NULL) delete curIndice;
        }
    }
    void draw(const GLuint shaderProgramID) {
        initializeOpenGLFunctions();
        setUp();

        // 曲线
        if (type == 8) {
            glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, rgb);
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            glBindVertexArray(curVAO);
            glDrawElements(GL_LINE_STRIP, CURVE_POINTS_NUM, GL_UNSIGNED_SHORT, NULL);
            glBindVertexArray(0);
            return ;
        }

        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        glBindVertexArray(VAO);  // 绑定顶点数据，绘制图元
        if (render_type == 1 || render_type == 2) {
            // 绘制线框加粗
            glEnable(GL_LINE_SMOOTH);
            glHint(GL_LINE_SMOOTH_HINT, GL_FASTEST/*GL_NICEST*/); // 抗锯齿
        } else {
            glDisable(GL_LINE_SMOOTH);
        }
        // 线框
        if (render_type == 1) {
            glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, rgb);
            glDrawElements(GL_LINE_LOOP, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
        }
        // 填充
        if (render_type == 0 || render_type == 2) {
            glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, rgb);
            if (type == 3)
                glDrawElements(GL_LINE_LOOP, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
            else
                glDrawElements(GL_POLYGON, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
        }
        // 填充带框
        if (render_type == 2 && type != 3) {
            float color[] = {0.0f, 0.0f, 0.0f};
            glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, color);
            glDrawElements(GL_LINE_LOOP, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
        }
        glBindVertexArray(0);               // 解绑顶点数据
    }

    void midPicDraw(const GLuint shaderProgramID) {
        initializeOpenGLFunctions();
        setUp();

        if (type == 8) {
            float color[] = {0.0f, 0.0f, 0.0f};
            glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, color);
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            glBindVertexArray(curVAO);
            glEnable(GL_LINE_STIPPLE);
            glLineStipple(2, 0x3333);
            glDrawElements(GL_LINE_STRIP, CURVE_POINTS_NUM, GL_UNSIGNED_SHORT, NULL);
            glDisable(GL_LINE_STIPPLE);
            glBindVertexArray(0);
        }

        glUniform3fv(glGetUniformLocation(shaderProgramID, "orgColor"), 1, rgb);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        glBindVertexArray(VAO);                        // 绑定顶点数据，绘制临时图元
        glEnable(GL_LINE_STIPPLE);
        glLineStipple(2, 0x3333/*0x5555*/);
        if (type == 8)
            glDrawElements(GL_LINE_STRIP, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
        else
            glDrawElements(GL_LINE_LOOP, i_size/sizeof(GLushort), GL_UNSIGNED_SHORT, NULL);
        glDisable(GL_LINE_STIPPLE);
        glBindVertexArray(0);                          // 解绑顶点数据
    }

    inline void setUp() {
        if (type == 8) {
            if (!hasCurVAO) {
                glGenVertexArrays(1, &curVAO);
                glGenBuffers(1, &curVBO);
                glGenBuffers(1, &curEBO);
                hasCurVAO = true;
            }
            // bezier curve
            if (curveVertex == NULL) {
                curveVertex = new GLfloat[3 * CURVE_POINTS_NUM];
                curIndice = new GLushort[CURVE_POINTS_NUM];
                for (unsigned int i = 0; i < CURVE_POINTS_NUM; ++i)
                    curIndice[i] = i;
            }
            GLfloat delta_t = 1.0 / CURVE_POINTS_NUM;
            unsigned int numOfPoints = v_size/sizeof(GLfloat)/3;
            for (int t = 0; t < CURVE_POINTS_NUM; ++t) {
                float t_map = t * delta_t;
                curveVertex[3*t + 0] = 0.0f;
                curveVertex[3*t + 1] = 0.0f;
                curveVertex[3*t + 2] = 0.0f;
                for (unsigned int i = 0; i < numOfPoints; ++i) {
                    GLfloat coefficient = C(numOfPoints - 1, i) * pow((1- t_map), numOfPoints - 1 - i) * pow(t_map, i);
                    curveVertex[3*t + 0] += coefficient * verts[3*i];
                    curveVertex[3*t + 1] += coefficient * verts[3*i + 1];
                }
            }
            glBindVertexArray(curVAO);
            glBindBuffer(GL_ARRAY_BUFFER, curVBO);
            glBufferData(GL_ARRAY_BUFFER, 3 * CURVE_POINTS_NUM * sizeof(GLfloat), curveVertex, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*3, 0);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, curEBO);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, CURVE_POINTS_NUM * sizeof(GLushort), curIndice, GL_STATIC_DRAW);
            glBindVertexArray(0);
        }

        if (!hasVAO) {
            // 初始化各种对象
            glGenVertexArrays(1, &VAO);
            glGenBuffers(1, &VBO);
            glGenBuffers(1, &EBO);
            // 各种对象已初始化
            hasVAO = true;
        }
        glBindVertexArray(VAO);                     // 绑定顶点数据
        glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定顶点缓冲对象
        glBufferData(GL_ARRAY_BUFFER, v_size, verts, GL_STATIC_DRAW);
                                                    //  GL_STATIC_DRAW  ：数据不会或几乎不会改变
                                                    //  GL_DYNAMIC_DRAW ：数据会被改变很多
                                                    //  GL_STREAM_DRAW  ：数据每次绘制时都会改变
        // 链接顶点属性
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);  // 绑定索引缓冲对象
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, i_size, indices, GL_STATIC_DRAW);
        glBindVertexArray(0);                        // 解绑顶点数据
    }

    void move(GLfloat x, GLfloat y) {
        // 平移
        for (unsigned int i = 0; i < i_size/sizeof(GLushort); ++i) {
            verts[0 + 3 * i] += x;
            verts[1 + 3 * i] += y;
        }
    }

    bool check_point(QPointF &pos) {
        switch(type) {
        case 0: break;
        case 1: return isInTria(pos); break;
        case 2: return isInRect(pos); break;
        case 3: return isInLine(pos); break;
        case 4: return isInRounRect(pos); break;
        case 5: return isInCirc(pos); break;
        case 6: return isInElli(pos); break;
        case 7: return isInPoly(pos); break;
        case 8: return isInCurv(pos); break;
        default: break;
        }
        return false;
    }

    GLboolean isInTria(QPointF &t) {
        GLfloat x_left = verts[6] < verts[3] ? verts[6] : verts[3];
        GLfloat x_right = verts[6] < verts[3] ? verts[3] : verts[6];
        GLfloat y_up = verts[7] < verts[1] ? verts[1] : verts[7];
        GLfloat y_down = verts[7] < verts[1] ? verts[7] : verts[1];
        if (t.x() < x_left || t.x() > x_right || t.y() > y_up || t.y() < y_down)
            return false;
        return true;
    }
    GLboolean isInRect(QPointF &t) {
        GLfloat x_left = verts[0] < verts[3] ? verts[0] : verts[3];
        GLfloat x_right = verts[0] < verts[3] ? verts[3] : verts[0];
        GLfloat y_up = verts[10] < verts[1] ? verts[1] : verts[10];
        GLfloat y_down = verts[10] < verts[1] ? verts[10] : verts[1];
        if (t.x() < x_left || t.x() > x_right || t.y() > y_up || t.y() < y_down)
            return false;
        return true;
    }
    GLboolean isInLine(QPointF &t) {
        // temporary solution
        GLfloat x_left = verts[0] < verts[3] ? verts[0] : verts[3];
        GLfloat x_right = verts[0] < verts[3] ? verts[3] : verts[0];
        GLfloat y_up = verts[1] < verts[4] ? verts[4] : verts[1];
        GLfloat y_down = verts[1] < verts[4] ? verts[1] : verts[4];
        if (t.x() < x_left || t.x() > x_right || t.y() < y_down || t.y() > y_up )
            return false;
        return true;
    }
    GLboolean isInRounRect(QPointF &t) {
        if (t.x() < verts[2 * CIRCLE_POINTS_NUM * 3] || t.x() > verts[0] ||
                t.y() < verts[3 * CIRCLE_POINTS_NUM * 3 + 1] || t.y() > verts[CIRCLE_POINTS_NUM * 3 + 1])
            return false;
        return true;
    }
    GLboolean isInCirc(QPointF &t) {
        GLfloat radius = (verts[0] - verts[2 * CIRCLE_POINTS_NUM * 3]) / 2;
        GLfloat centralX = verts[2 * CIRCLE_POINTS_NUM * 3] + (verts[0] - verts[2 * CIRCLE_POINTS_NUM * 3])/2;
        GLfloat centralY = verts[3 * CIRCLE_POINTS_NUM * 3 + 1] + (verts[CIRCLE_POINTS_NUM * 3 + 1] - verts[3 * CIRCLE_POINTS_NUM * 3 + 1])/2;
        if (radius * radius <= (t.x() - centralX) * (t.x() - centralX) + (t.y() - centralY) * (t.y() - centralY))
            return false;
        return true;
    }
    GLboolean isInElli(QPointF &t) {
        if (t.x() < verts[2 * CIRCLE_POINTS_NUM * 3] || t.x() > verts[0] ||
                t.y() < verts[3 * CIRCLE_POINTS_NUM * 3 + 1] || t.y() > verts[CIRCLE_POINTS_NUM * 3 + 1])
            return false;
        return true;
    }
    GLboolean isInPoly(QPointF &t) {
        // How to DO? temporary solution
        float x_left = verts[0];
        float x_right = verts[0];
        float y_up = verts[1];
        float y_down = verts[1];
        for (unsigned int i = 0; i < v_size/sizeof(GLfloat); i += 3) {
            if (verts[i] < x_left) x_left = verts[i];
            if (verts[i] > x_right) x_right = verts[i];
            if (verts[i + 1] < y_down) y_down = verts[i + 1];
            if (verts[i + 1] > y_up) y_up = verts[i + 1];
        }
        if (t.x() < x_left || t.x() > x_right || t.y() < y_down || t.y() > y_up )
            return false;
        return true;
    }
    GLboolean isInCurv(QPointF &t) {
        // temporary solution, too
        float x_left = verts[0];
        float x_right = verts[0];
        float y_up = verts[1];
        float y_down = verts[1];
        for (unsigned int i = 0; i < v_size/sizeof(GLfloat); i += 3) {
            if (verts[i] < x_left) x_left = verts[i];
            if (verts[i] > x_right) x_right = verts[i];
            if (verts[i + 1] < y_down) y_down = verts[i + 1];
            if (verts[i + 1] > y_up) y_up = verts[i + 1];
        }
        if (t.x() < x_left || t.x() > x_right || t.y() < y_down || t.y() > y_up )
            return false;
        return true;
    }

    void scale(float x, float y) {
        QMatrix4x4 m;
        m.setToIdentity();
        m.scale(QVector3D(x, y, 1.0f));
        QVector4D t, result;
        t.setZ(0.0f);
        t.setW(1.0f);
        for (unsigned int i = 0; i < v_size/sizeof(GLfloat); i+= 3) {
            t.setX(verts[i]);
            t.setY(verts[i + 1]);
            result = m * t;
            verts[i] = result.x();
            verts[i + 1] = result.y();
        }
    }
};

}

class OpenGLPaintingArea : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Compatibility
{
public:
    OpenGLPaintingArea(QWidget *parent);
    virtual ~OpenGLPaintingArea();

    void installShader();
    void keyPressEvent(QKeyEvent *event);

protected:
    void initializeGL();
    void paintGL();
    void resizeGL(int w,int h);

    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
    void mouseDoubleClickEvent(QMouseEvent *event);
    void wheelEvent(QWheelEvent *event);

private:
    GLuint shaderProgramID;
};

#endif // OPENGLPAINTINGAREA_H
